# Copyright (c) 2010
# The Regents of the University of Michigan
# All Rights Reserved

# Permission is granted to use, copy, create derivative works, and
# redistribute this software and such derivative works for any purpose,
# so long as the name of the University of Michigan is not used in
# any advertising or publicity pertaining to the use or distribution
# of this software without specific, written prior authorization. If
# the above copyright notice or any other identification of the
# University of Michigan is included in any copy of any portion of
# this software, then the disclaimer below must also be included.

# This software is provided as is, without representation or warranty
# of any kind either express or implied, including without limitation
# the implied warranties of merchantability, fitness for a particular
# purpose, or noninfringement.  The Regents of the University of
# Michigan shall not be liable for any damages, including special,
# indirect, incidental, or consequential damages, with respect to any
# claim arising out of or in connection with the use of the software,
# even if it has been or is hereafter advised of the possibility of
# such damages.

from pydht import dhtutil
import serialization

import dhtclienthelpers

from serialization import RequestFindNode, ACT_REQUEST_FIND_NODE # NEED SPEED
from serialization import encode_find_node_request

import event

import array
import bisect
import collections
import hashlib
import logging
from random import randint, getrandbits
import socket
from time import time
import types


class VuzeUtilError(Exception): pass


MAX_RETRIES = 1


BOOTSTRAP_NODES = [(socket.gethostbyname('dht.aelitis.com'), 6881)]


NAME = 'Vuze'


class DHTClient(dhtutil.BaseClient):
  '''One instance of a client for the Vuze DHT.'''


  def __init__(self, address, **kwargs):
    dhtutil.BaseClient.__init__(self, address)
    self.transaction_id = randint(0, 2**32-1)
    self.instance_id = randint(0, 2**32-1)
    self.dht_id = compute_node_id(*self.address)
    self.packetologist = dhtclienthelpers.VuzePacketologist(self.address)

  @staticmethod
  def _get_connection_id():
    return getrandbits(64) | 0x8000000000000000

  def _build_request_header(self, action, conn_id):
    spv = serialization.SUPPORTED_PROTOCOL_VERSION
    header = serialization.RequestHeader(
      conn_id,
      action,
      self.transaction_id,
      spv,
      0,
      0,
      spv,
      self.address,
      self.instance_id,
      int(time() * 1000))

    self.transaction_id += 1
    return header

  def _build_reply_header(self, action, request_header):
    return serialization.ReplyHeader(
        action,
        request_header.transaction_id,
        request_header.connection_id,
        serialization.SUPPORTED_PROTOCOL_VERSION,
        0,
        0,
        self.instance_id)

  def _process_packet(self, packet, sender):
    try:
      # In Vuze, the contact is the same as the contact address, so we
      # explicitly pass that through, here.
      serialization.decode_packet(packet, self, sender,
                                  make_contact(sender))
      return True
    except serialization.SerializationError as e:
      logging.error('Bad packet from %s:%d: %s' % (sender[0], sender[1], e))
      return False

  def _handle_recv(self):
    packet, sender = self.sock.recvfrom(8192)
    self._process_packet(packet, sender)
    return True  # Reschedule.

  def send_ping_request(self, other_address):
#    conn_id = self._get_connection_id()
    conn_id = getrandbits(64) | 0x8000000000000000
    request = serialization.RequestPing(
        self._build_request_header(serialization.ACT_REQUEST_PING, conn_id))
    request = serialization.encode_ping_request(request)
    self._send(request, other_address, conn_id)

  def send_stats_request(self, other_address):
    conn_id = getrandbits(64) | 0x8000000000000000
    request = serialization.RequestStats(
        self._build_request_header(serialization.ACT_REQUEST_STATS, conn_id))
    request = serialization.encode_stats_request(request)
    self._send(request, other_address, conn_id)

  def send_ping_reply(self, other_address, request_header):
    reply = serialization.ReplyPing(
        self._build_reply_header(serialization.ACT_REPLY_PING, request_header))
    reply = serialization.encode_ping_reply(reply)
    self._send(reply, other_address, request_header.connection_id)

  def send_store_request(self, other_address, key, value, version, spoof_id):
    conn_id = getrandbits(64) | 0x8000000000000000
    request = serialization.RequestStore(
      self._build_request_header(serialization.ACT_REQUEST_STORE, conn_id),
      spoof_id,
      [key],
      [[serialization.TransportValue(version, value, self.address, 1)]])
    request = serialization.encode_store_request(request)
    self._send(request, other_address, conn_id)

  def send_store_reply(self, other_address, request_header):
    reply = serialization.ReplyStore(
        self._build_reply_header(serialization.ACT_REPLY_STORE, request_header),
        [])
    reply = serialization.encode_store_reply(reply)
    self._send(reply, other_address, request_header.connection_id)

  def send_data_request(self, other_address, transfer_key, key, conn_id, pos=0, length=0):
    request = serialization.Data(
        self._build_request_header(serialization.ACT_DATA, conn_id),
        serialization.PT_READ_REQUEST, transfer_key,
        key, pos, length, 0, str())
    request = serialization.encode_data(request)
    self._send(request, other_address, conn_id)


  def send_find_node(self, other_address, id_to_find):
#    conn_id = self._get_connection_id()
    conn_id = getrandbits(64) | 0x8000000000000000
    hdr = self._build_request_header(1028, #ACT_REQUEST_FIND_NODE,
                                     conn_id)
    request = encode_find_node_request(RequestFindNode(hdr, id_to_find))
    self._send(request, other_address, conn_id)

  def send_find_value(self, other_address, id_to_find):
    conn_id = getrandbits(64) | 0x8000000000000000
    hdr = self._build_request_header(serialization.ACT_REQUEST_FIND_VALUE,
                                     conn_id)
    request = serialization.encode_find_value_request(
        serialization.RequestFindValue(hdr, id_to_find, 1, 255))
    self._send(request, other_address, conn_id)
                                                      

DHTClient.send_find_node = types.MethodType(
    dhtclienthelpers.opt_send_find_node, None, DHTClient)


def compute_node_id(ip, port=None):
  '''Return a byte string of the ID of the node with given ip and port.'''
  if port is None:
    assert isinstance(ip, tuple)
    port = ip[1]
    ip = ip[0]
  return hashlib.sha1(ip + ':' + str(port)).digest()


def compute_hex_node_id(ip, port):
  '''Return a hex string of the ID of the node with given ip and port.'''
  return hashlib.sha1(ip + ':' + str(port)).hexdigest()


def brute_force_id_with_bits(ip_addresses, desired_prefix, nbits,
                             have_root=False):
  '''Find a port number on some given IP such that node ID has given prefix.
  ip_address is a list of available IP addresses in dotted quad format.
  Desired prefix is a byte string indicating the prefix we want for the ID.
  nbits is the number of significant bits in the prefix; must be <=
  len(desired_prefix) * 8.
  '''

  if len(desired_prefix) < 20:
    desired_prefix += '\x00' * (20 - len(desired_prefix))

  for ip_address in ip_addresses:
    for port in xrange(1 if have_root else 1025,65536):
      if dhtutil.compute_shared_prefix_bits(compute_node_id(ip_address, port),
                                            desired_prefix) >= nbits:
        return ip_address, port

  return None


def brute_force_closest_id(ip_addresses, target_id, first_port, last_port):
  '''Find a port number on some given IP such that node ID has given prefix.
  ip_address is a list of available IP addresses in dotted quad format.
  Desired prefix is a byte string indicating the prefix we want for the ID.
  nbits is the number of significant bits in the prefix; must be <=
  len(desired_prefix) * 8.
  '''

  cur_bits = 0
  cur_best = None

  for ip_address in ip_addresses:
    for port in xrange(first_port, last_port + 1):
      cand_bits = compute_shared_prefix_bits(compute_node_id(ip_address, port),
                                    target_id)
      if cand_bits > cur_bits:
        cur_best = (ip_address, port)
        cur_bits = cand_bits

  return cur_best


def is_peer_record(s):
  '''Return True iff s is a peer record.
  Peer record format: [ip_override:]tcp_port[;C][;udp_port]
  '''

  if len(s) > 29:
    return None

  colon_idx = s.find(':')
  if colon_idx != -1:
    ip_part = s[:colon_idx]
    try:
      socket.inet_aton(ip_part)
    except socket.error:
      return None
    except TypeError:
      return None
    s = s[colon_idx+1:]
  
  first_semicolon_idx = s.find(';')
  if first_semicolon_idx == -1:
    first_semicolon_idx = len(s)

  # Check the TCP port.
  ret = None

  try:
    ret = int(s[:first_semicolon_idx])
    if not 0 <= ret <= 65535:
      return None
  except ValueError:
    return None


  s = s[first_semicolon_idx+1:]
  if not s:
    return None

  if s == 'C':
    return ret

  if s.startswith('C;'):
    s = s[2:]
    # Check the UDP port.
  try:
    if not 0 <= int(s) <= 65535:
      return None
  except ValueError:
    return None

  return ret


def is_torrent_description(val):
  try:
    val = libtorrent.bdecode(val)
  except RuntimeError:
    return False

  return (val is not None and isinstance(val, dict) and 's' in val
          and 'r' in val and 'd' in val and ('h' in val or 't' in val))


def make_contact(contact):
  return contact


def get_contact_address(contact):
  return contact
